import { TimeClass } from "../../core/type/Time.js";
import { TimelineValue } from "../../core/util/TimelineValue.js";
import { Pow } from "../../signal/Pow.js";
import { onContextClose, onContextInit, } from "../context/ContextInitialization.js";
import { Gain } from "../context/Gain.js";
import { ToneWithContext, } from "../context/ToneWithContext.js";
import { TicksClass } from "../type/Ticks.js";
import { TransportTimeClass } from "../type/TransportTime.js";
import { enterScheduledCallback } from "../util/Debug.js";
import { optionsFromArguments } from "../util/Defaults.js";
import { Emitter } from "../util/Emitter.js";
import { readOnly, writable } from "../util/Interface.js";
import { IntervalTimeline } from "../util/IntervalTimeline.js";
import { Timeline } from "../util/Timeline.js";
import { isArray, isDefined } from "../util/TypeCheck.js";
import { Clock } from "./Clock.js";
import { TransportEvent } from "./TransportEvent.js";
import { TransportRepeatEvent } from "./TransportRepeatEvent.js";
/**
 * Transport for timing musical events.
 * Supports tempo curves and time changes. Unlike browser-based timing (setInterval, requestAnimationFrame)
 * Transport timing events pass in the exact time of the scheduled event
 * in the argument of the callback function. Pass that time value to the object
 * you're scheduling. <br><br>
 * A single transport is created for you when the library is initialized.
 * <br><br>
 * The transport emits the events: "start", "stop", "pause", and "loop" which are
 * called with the time of that event as the argument.
 *
 * @example
 * const osc = new Tone.Oscillator().toDestination();
 * // repeated event every 8th note
 * Tone.getTransport().scheduleRepeat((time) => {
 * 	// use the callback time to schedule events
 * 	osc.start(time).stop(time + 0.1);
 * }, "8n");
 * // transport must be started before it starts invoking events
 * Tone.getTransport().start();
 * @category Core
 */
export class TransportClass extends ToneWithContext {
    constructor() {
        const options = optionsFromArguments(TransportClass.getDefaults(), arguments);
        super(options);
        this.name = "Transport";
        //-------------------------------------
        // 	LOOPING
        //-------------------------------------
        /**
         * If the transport loops or not.
         */
        this._loop = new TimelineValue(false);
        /**
         * The loop start position in ticks
         */
        this._loopStart = 0;
        /**
         * The loop end position in ticks
         */
        this._loopEnd = 0;
        //-------------------------------------
        // 	TIMELINE EVENTS
        //-------------------------------------
        /**
         * All the events in an object to keep track by ID
         */
        this._scheduledEvents = {};
        /**
         * The scheduled events.
         */
        this._timeline = new Timeline();
        /**
         * Repeated events
         */
        this._repeatedEvents = new IntervalTimeline();
        /**
         * All of the synced Signals
         */
        this._syncedSignals = [];
        /**
         * The swing amount
         */
        this._swingAmount = 0;
        // CLOCK/TEMPO
        this._ppq = options.ppq;
        this._clock = new Clock({
            callback: this._processTick.bind(this),
            context: this.context,
            frequency: 0,
            units: "bpm",
        });
        this._bindClockEvents();
        this.bpm = this._clock.frequency;
        this._clock.frequency.multiplier = options.ppq;
        this.bpm.setValueAtTime(options.bpm, 0);
        readOnly(this, "bpm");
        this._timeSignature = options.timeSignature;
        // SWING
        this._swingTicks = options.ppq / 2; // 8n
    }
    static getDefaults() {
        return Object.assign(ToneWithContext.getDefaults(), {
            bpm: 120,
            loopEnd: "4m",
            loopStart: 0,
            ppq: 192,
            swing: 0,
            swingSubdivision: "8n",
            timeSignature: 4,
        });
    }
    //-------------------------------------
    // 	TICKS
    //-------------------------------------
    /**
     * called on every tick
     * @param  tickTime clock relative tick time
     */
    _processTick(tickTime, ticks) {
        // do the loop test
        if (this._loop.get(tickTime)) {
            if (ticks >= this._loopEnd) {
                this.emit("loopEnd", tickTime);
                this._clock.setTicksAtTime(this._loopStart, tickTime);
                ticks = this._loopStart;
                this.emit("loopStart", tickTime, this._clock.getSecondsAtTime(tickTime));
                this.emit("loop", tickTime);
            }
        }
        // handle swing
        if (this._swingAmount > 0 &&
            ticks % this._ppq !== 0 && // not on a downbeat
            ticks % (this._swingTicks * 2) !== 0) {
            // add some swing
            const progress = (ticks % (this._swingTicks * 2)) / (this._swingTicks * 2);
            const amount = Math.sin(progress * Math.PI) * this._swingAmount;
            tickTime +=
                new TicksClass(this.context, (this._swingTicks * 2) / 3).toSeconds() * amount;
        }
        // invoke the timeline events scheduled on this tick
        enterScheduledCallback(true);
        this._timeline.forEachAtTime(ticks, (event) => event.invoke(tickTime));
        enterScheduledCallback(false);
    }
    //-------------------------------------
    // 	SCHEDULABLE EVENTS
    //-------------------------------------
    /**
     * Schedule an event along the timeline.
     * @param callback The callback to be invoked at the time.
     * @param time The time to invoke the callback at.
     * @return The id of the event which can be used for canceling the event.
     * @example
     * // schedule an event on the 16th measure
     * Tone.getTransport().schedule((time) => {
     * 	// invoked on measure 16
     * 	console.log("measure 16!");
     * }, "16:0:0");
     */
    schedule(callback, time) {
        const event = new TransportEvent(this, {
            callback,
            time: new TransportTimeClass(this.context, time).toTicks(),
        });
        return this._addEvent(event, this._timeline);
    }
    /**
     * Schedule a repeated event along the timeline. The event will fire
     * at the `interval` starting at the `startTime` and for the specified
     * `duration`.
     * @param  callback   The callback to invoke.
     * @param  interval   The duration between successive callbacks. Must be a positive number.
     * @param  startTime  When along the timeline the events should start being invoked.
     * @param  duration How long the event should repeat.
     * @return  The ID of the scheduled event. Use this to cancel the event.
     * @example
     * const osc = new Tone.Oscillator().toDestination().start();
     * // a callback invoked every eighth note after the first measure
     * Tone.getTransport().scheduleRepeat((time) => {
     * 	osc.start(time).stop(time + 0.1);
     * }, "8n", "1m");
     */
    scheduleRepeat(callback, interval, startTime, duration = Infinity) {
        const event = new TransportRepeatEvent(this, {
            callback,
            duration: new TimeClass(this.context, duration).toTicks(),
            interval: new TimeClass(this.context, interval).toTicks(),
            time: new TransportTimeClass(this.context, startTime).toTicks(),
        });
        // kick it off if the Transport is started
        // @ts-ignore
        return this._addEvent(event, this._repeatedEvents);
    }
    /**
     * Schedule an event that will be removed after it is invoked.
     * @param callback The callback to invoke once.
     * @param time The time the callback should be invoked.
     * @returns The ID of the scheduled event.
     */
    scheduleOnce(callback, time) {
        const event = new TransportEvent(this, {
            callback,
            once: true,
            time: new TransportTimeClass(this.context, time).toTicks(),
        });
        return this._addEvent(event, this._timeline);
    }
    /**
     * Clear the passed in event id from the timeline
     * @param eventId The id of the event.
     */
    clear(eventId) {
        if (this._scheduledEvents.hasOwnProperty(eventId)) {
            const item = this._scheduledEvents[eventId.toString()];
            item.timeline.remove(item.event);
            item.event.dispose();
            delete this._scheduledEvents[eventId.toString()];
        }
        return this;
    }
    /**
     * Add an event to the correct timeline. Keep track of the
     * timeline it was added to.
     * @returns the event id which was just added
     */
    _addEvent(event, timeline) {
        this._scheduledEvents[event.id.toString()] = {
            event,
            timeline,
        };
        timeline.add(event);
        return event.id;
    }
    /**
     * Remove scheduled events from the timeline after
     * the given time. Repeated events will be removed
     * if their startTime is after the given time
     * @param after Clear all events after this time.
     */
    cancel(after = 0) {
        const computedAfter = this.toTicks(after);
        this._timeline.forEachFrom(computedAfter, (event) => this.clear(event.id));
        this._repeatedEvents.forEachFrom(computedAfter, (event) => this.clear(event.id));
        return this;
    }
    //-------------------------------------
    // 	START/STOP/PAUSE
    //-------------------------------------
    /**
     * Bind start/stop/pause events from the clock and emit them.
     */
    _bindClockEvents() {
        this._clock.on("start", (time, offset) => {
            offset = new TicksClass(this.context, offset).toSeconds();
            this.emit("start", time, offset);
        });
        this._clock.on("stop", (time) => {
            this.emit("stop", time);
        });
        this._clock.on("pause", (time) => {
            this.emit("pause", time);
        });
    }
    /**
     * Returns the playback state of the source, either "started", "stopped", or "paused"
     */
    get state() {
        return this._clock.getStateAtTime(this.now());
    }
    /**
     * Start the transport and all sources synced to the transport.
     * @param  time The time when the transport should start.
     * @param  offset The timeline offset to start the transport.
     * @example
     * // start the transport in one second starting at beginning of the 5th measure.
     * Tone.getTransport().start("+1", "4:0:0");
     */
    start(time, offset) {
        // start the context
        this.context.resume();
        let offsetTicks;
        if (isDefined(offset)) {
            offsetTicks = this.toTicks(offset);
        }
        // start the clock
        this._clock.start(time, offsetTicks);
        return this;
    }
    /**
     * Stop the transport and all sources synced to the transport.
     * @param time The time when the transport should stop.
     * @example
     * Tone.getTransport().stop();
     */
    stop(time) {
        this._clock.stop(time);
        return this;
    }
    /**
     * Pause the transport and all sources synced to the transport.
     */
    pause(time) {
        this._clock.pause(time);
        return this;
    }
    /**
     * Toggle the current state of the transport. If it is
     * started, it will stop it, otherwise it will start the Transport.
     * @param  time The time of the event
     */
    toggle(time) {
        time = this.toSeconds(time);
        if (this._clock.getStateAtTime(time) !== "started") {
            this.start(time);
        }
        else {
            this.stop(time);
        }
        return this;
    }
    //-------------------------------------
    // 	SETTERS/GETTERS
    //-------------------------------------
    /**
     * The time signature as just the numerator over 4.
     * For example 4/4 would be just 4 and 6/8 would be 3.
     * @example
     * // common time
     * Tone.getTransport().timeSignature = 4;
     * // 7/8
     * Tone.getTransport().timeSignature = [7, 8];
     * // this will be reduced to a single number
     * Tone.getTransport().timeSignature; // returns 3.5
     */
    get timeSignature() {
        return this._timeSignature;
    }
    set timeSignature(timeSig) {
        if (isArray(timeSig)) {
            timeSig = (timeSig[0] / timeSig[1]) * 4;
        }
        this._timeSignature = timeSig;
    }
    /**
     * When the Transport.loop = true, this is the starting position of the loop.
     */
    get loopStart() {
        return new TimeClass(this.context, this._loopStart, "i").toSeconds();
    }
    set loopStart(startPosition) {
        this._loopStart = this.toTicks(startPosition);
    }
    /**
     * When the Transport.loop = true, this is the ending position of the loop.
     */
    get loopEnd() {
        return new TimeClass(this.context, this._loopEnd, "i").toSeconds();
    }
    set loopEnd(endPosition) {
        this._loopEnd = this.toTicks(endPosition);
    }
    /**
     * If the transport loops or not.
     */
    get loop() {
        return this._loop.get(this.now());
    }
    set loop(loop) {
        this._loop.set(loop, this.now());
    }
    /**
     * Set the loop start and stop at the same time.
     * @example
     * // loop over the first measure
     * Tone.getTransport().setLoopPoints(0, "1m");
     * Tone.getTransport().loop = true;
     */
    setLoopPoints(startPosition, endPosition) {
        this.loopStart = startPosition;
        this.loopEnd = endPosition;
        return this;
    }
    /**
     * The swing value. Between 0-1 where 1 equal to the note + half the subdivision.
     */
    get swing() {
        return this._swingAmount;
    }
    set swing(amount) {
        // scale the values to a normal range
        this._swingAmount = amount;
    }
    /**
     * Set the subdivision which the swing will be applied to.
     * The default value is an 8th note. Value must be less
     * than a quarter note.
     */
    get swingSubdivision() {
        return new TicksClass(this.context, this._swingTicks).toNotation();
    }
    set swingSubdivision(subdivision) {
        this._swingTicks = this.toTicks(subdivision);
    }
    /**
     * The Transport's position in Bars:Beats:Sixteenths.
     * Setting the value will jump to that position right away.
     */
    get position() {
        const now = this.now();
        const ticks = this._clock.getTicksAtTime(now);
        return new TicksClass(this.context, ticks).toBarsBeatsSixteenths();
    }
    set position(progress) {
        const ticks = this.toTicks(progress);
        this.ticks = ticks;
    }
    /**
     * The Transport's position in seconds.
     * Setting the value will jump to that position right away.
     */
    get seconds() {
        return this._clock.seconds;
    }
    set seconds(s) {
        const now = this.now();
        const ticks = this._clock.frequency.timeToTicks(s, now);
        this.ticks = ticks;
    }
    /**
     * The Transport's loop position as a normalized value. Always
     * returns 0 if the Transport.loop = false.
     */
    get progress() {
        if (this.loop) {
            const now = this.now();
            const ticks = this._clock.getTicksAtTime(now);
            return ((ticks - this._loopStart) / (this._loopEnd - this._loopStart));
        }
        else {
            return 0;
        }
    }
    /**
     * The Transport's current tick position.
     */
    get ticks() {
        return this._clock.ticks;
    }
    set ticks(t) {
        if (this._clock.ticks !== t) {
            const now = this.now();
            // stop everything synced to the transport
            if (this.state === "started") {
                const ticks = this._clock.getTicksAtTime(now);
                // schedule to start on the next tick, #573
                const remainingTick = this._clock.frequency.getDurationOfTicks(Math.ceil(ticks) - ticks, now);
                const time = now + remainingTick;
                this.emit("stop", time);
                this._clock.setTicksAtTime(t, time);
                // restart it with the new time
                this.emit("start", time, this._clock.getSecondsAtTime(time));
            }
            else {
                this.emit("ticks", now);
                this._clock.setTicksAtTime(t, now);
            }
        }
    }
    /**
     * Get the clock's ticks at the given time.
     * @param  time  When to get the tick value
     * @return The tick value at the given time.
     */
    getTicksAtTime(time) {
        return this._clock.getTicksAtTime(time);
    }
    /**
     * Return the elapsed seconds at the given time.
     * @param  time  When to get the elapsed seconds
     * @return  The number of elapsed seconds
     */
    getSecondsAtTime(time) {
        return this._clock.getSecondsAtTime(time);
    }
    /**
     * Pulses Per Quarter note. This is the smallest resolution
     * the Transport timing supports. This should be set once
     * on initialization and not set again. Changing this value
     * after other objects have been created can cause problems.
     */
    get PPQ() {
        return this._clock.frequency.multiplier;
    }
    set PPQ(ppq) {
        this._clock.frequency.multiplier = ppq;
    }
    //-------------------------------------
    // 	SYNCING
    //-------------------------------------
    /**
     * Returns the time aligned to the next subdivision
     * of the Transport. If the Transport is not started,
     * it will return 0.
     * Note: this will not work precisely during tempo ramps.
     * @param  subdivision  The subdivision to quantize to
     * @return  The context time of the next subdivision.
     * @example
     * // the transport must be started, otherwise returns 0
     * Tone.getTransport().start();
     * Tone.getTransport().nextSubdivision("4n");
     */
    nextSubdivision(subdivision) {
        subdivision = this.toTicks(subdivision);
        if (this.state !== "started") {
            // if the transport's not started, return 0
            return 0;
        }
        else {
            const now = this.now();
            // the remainder of the current ticks and the subdivision
            const transportPos = this.getTicksAtTime(now);
            const remainingTicks = subdivision - (transportPos % subdivision);
            return this._clock.nextTickTime(remainingTicks, now);
        }
    }
    /**
     * Attaches the signal to the tempo control signal so that
     * any changes in the tempo will change the signal in the same
     * ratio.
     *
     * @param signal
     * @param ratio Optionally pass in the ratio between the two signals.
     * 			Otherwise it will be computed based on their current values.
     */
    syncSignal(signal, ratio) {
        const now = this.now();
        let source = this.bpm;
        let sourceValue = 1 / (60 / source.getValueAtTime(now) / this.PPQ);
        let nodes = [];
        // If the signal is in the time domain, sync it to the reciprocal of
        // the tempo instead of the tempo.
        if (signal.units === "time") {
            // The input to Pow should be in the range [1 / 4096, 1], where
            // where 4096 is half of the buffer size of Pow's waveshaper.
            // Pick a scaling factor based on the initial tempo that ensures
            // that the initial input is in this range, while leaving room for
            // tempo changes.
            const scaleFactor = 1 / 64 / sourceValue;
            const scaleBefore = new Gain(scaleFactor);
            const reciprocal = new Pow(-1);
            const scaleAfter = new Gain(scaleFactor);
            // @ts-ignore
            source.chain(scaleBefore, reciprocal, scaleAfter);
            source = scaleAfter;
            sourceValue = 1 / sourceValue;
            nodes = [scaleBefore, reciprocal, scaleAfter];
        }
        if (!ratio) {
            // get the sync ratio
            if (signal.getValueAtTime(now) !== 0) {
                ratio = signal.getValueAtTime(now) / sourceValue;
            }
            else {
                ratio = 0;
            }
        }
        const ratioSignal = new Gain(ratio);
        // @ts-ignore
        source.connect(ratioSignal);
        // @ts-ignore
        ratioSignal.connect(signal._param);
        nodes.push(ratioSignal);
        this._syncedSignals.push({
            initial: signal.value,
            nodes: nodes,
            signal,
        });
        signal.value = 0;
        return this;
    }
    /**
     * Unsyncs a previously synced signal from the transport's control.
     * @see {@link syncSignal}.
     */
    unsyncSignal(signal) {
        for (let i = this._syncedSignals.length - 1; i >= 0; i--) {
            const syncedSignal = this._syncedSignals[i];
            if (syncedSignal.signal === signal) {
                syncedSignal.nodes.forEach((node) => node.dispose());
                syncedSignal.signal.value = syncedSignal.initial;
                this._syncedSignals.splice(i, 1);
            }
        }
        return this;
    }
    /**
     * Clean up.
     */
    dispose() {
        super.dispose();
        this._clock.dispose();
        writable(this, "bpm");
        this._timeline.dispose();
        this._repeatedEvents.dispose();
        return this;
    }
}
Emitter.mixin(TransportClass);
//-------------------------------------
// 	INITIALIZATION
//-------------------------------------
onContextInit((context) => {
    context.transport = new TransportClass({ context });
});
onContextClose((context) => {
    context.transport.dispose();
});
//# sourceMappingURL=Transport.js.map